package org.swift.mashup.engine.deployment;

import java.io.*;
import java.net.URL;
import java.util.*;
import java.util.regex.Pattern;

import javax.xml.stream.XMLStreamException;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;

import org.apache.axiom.om.OMElement;
import org.apache.axis2.AxisFault;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.deployment.Deployer;
import org.apache.axis2.deployment.DeploymentErrorMsgs;
import org.apache.axis2.deployment.DeploymentException;
import org.apache.axis2.deployment.repository.util.DeploymentFileData;
import org.apache.axis2.deployment.util.Utils;
import org.apache.axis2.description.AxisService;
import org.apache.axis2.i18n.Messages;
import org.apache.axis2.util.XMLUtils;
import org.apache.log4j.Logger;
import org.swift.commons.deployer.framework.axis2.AxisOperationExposure;
import org.swift.commons.deployer.framework.axis2.AxisServiceFactory;
import org.swift.mashup.engine.deployment.mcr.*;
import org.swift.mashup.engine.model.Mashup;
import org.swift.mashup.engine.model.MashupImpl;
import org.swift.mashup.engine.util.XPathUtil;
import org.swift.mashup.engine.visualization.MashupVisualizationFactory;
import org.swift.mashup.engine.visualization.Visualization;
import org.swift.mashup.engine.visualization.VisualizationFactory;
import org.swift.mashup.engine.visualization.VisualizationManager;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

@SuppressWarnings("unchecked")
public class MashupDeployer implements Deployer {
	private static Logger log = Logger.getLogger(MashupDeployer.class);

	private static String MASHUP_FILE_EXTENSION = "XML";
	private static final String PROP_DEPLOYMENT_EXCLUSION_PATTERN = MashupDeployer.class.getName() + ".deploymentExclusionPattern";
	private static final String DEFAULT_DEPLOYMENT_EXCLUSION_PATTERN = "^_.*|.*\\.comp$";

	@SuppressWarnings("unused")
	private String directory = null;

	private ConfigurationContext configCtx;

	private String NAMESPACE_PREFIX = "cbmpre";

	private String NAMESPACE = "http://www.cbmplusre.com/mashups";

	private AxisOperationExposure mashupFacade = null;

	private VisualizationFactory<OMElement> visualizationFactory = null;
	
	public MashupDeployer() {
		mashupFacade = new MashupHttpFacade();
		visualizationFactory = new MashupVisualizationFactory();
	}

	@Override
	public void deploy(DeploymentFileData deploymentFileData)
			throws DeploymentException {
		if (isMashupFile(deploymentFileData.getFile())) {
			final String mashupName = getMashupName(deploymentFileData.getFile());
			InputStream fileInputStream = null;
			try {
				Properties props = getDeployerProperties(deploymentFileData.getFile().getParent());
				if ( matchesDeploymentExclusionPattern(mashupName, props) ) {
					log.info(String.format("Mashup '%s' is excluded from deployment",mashupName));
				}
				else {
					log.debug("Deploying mashup with name " + mashupName);
					URL fileURL = deploymentFileData.getFile().toURI().toURL();
					fileInputStream = new FileInputStream(deploymentFileData.getFile());
					removeFaultyService(deploymentFileData.getFile());
					Mashup mashup = new MashupImpl(mashupName);
					
					List<MashupPreprocessor> processors = new ArrayList<MashupPreprocessor>();
					MashupLocations.getInstance().put(mashupName+"."+MASHUP_FILE_EXTENSION, deploymentFileData.getFile().getAbsolutePath());
					MashupImportPreprocessor mip = new MashupImportPreprocessor(
							deploymentFileData.getFile().getParent(), resolveMashupDirectory(),
                            MashupDependentsMap.getInstance(), MashupLocations.getInstance());
					processors.add( mip );
					processors.add( new MashupVariablePreprocessor() );

					for ( MashupPreprocessor processor : processors ) {
						InputStream processedStream = processor.processMashup(fileInputStream, mashupName);

						if ( !fileInputStream.equals(processedStream) ) {
							closeStream(fileInputStream);
							fileInputStream = processedStream;
						}
					}

					mashup.load(fileInputStream);

					AxisService service = createAxisService(
							mashupName, mashup,
							fileURL, getClassLoader());
					service.addParameter(MashupSchemaGenerator.MASHUP_PROPERTY,
							mashup);

					// It's not thread safe :(
					MashupSchemaGenerator schemaGenerator = new MashupSchemaGenerator(
							NAMESPACE, NAMESPACE_PREFIX);
					schemaGenerator.generateSchema(service);

                    // no longer needed after schema generation
                    service.getParameter(MashupSchemaGenerator.MASHUP_PROPERTY).setValue(null);

					configCtx.getAxisConfiguration().addService(service);

					buildVisualizations(service.getName(), deploymentFileData.getFile());

					if ( log.isInfoEnabled() ) {
						log.info( String.format("Deployed mashup '%s'",mashupName) );
					}
				}

				redeployDependentMashups(mashupName, deploymentFileData);
			} catch (Exception e) {
				log.error("Error when deploying mashup " + mashupName + ":", e);
				storeFaultyService(deploymentFileData, e);
			}catch (Throwable t) {
				log.info("Error when deploying mashup " + mashupName + ":", t);
				storeFaultyService(deploymentFileData, t);
			} finally {
				closeStream(fileInputStream);
			}
		}
	}

	private void closeStream(InputStream fileInputStream) {
		if (fileInputStream != null) {
			try {
				fileInputStream.close();
			} catch (IOException e) {
				log.error("Error when closing mashup inputstream:", e);
			}
		}
	}

	private void redeployDependentMashups(String mashupName, DeploymentFileData deploymentFileData)
			throws DeploymentException {

		Set<String> redeployMuxes = MashupDependentsMap.getInstance().getOutOfDateDependents(
				mashupName, deploymentFileData.getFile().lastModified() );

		for(String rmux : redeployMuxes){
			if ( log.isInfoEnabled() ) {
				log.info( String.format("Redeploying mashup '%s' due to change in '%s'",rmux,mashupName) );
			}
			String location = MashupLocations.getInstance().get(rmux+"."+MASHUP_FILE_EXTENSION);
			File rmuxFile = new File(location);
			DeploymentFileData rmuxDeployFileData = new DeploymentFileData(rmuxFile,this);
			rmuxDeployFileData.setClassLoader(deploymentFileData.getClassLoader());
			unDeployDependent(location);
			deploy(rmuxDeployFileData);
		}
	}

	protected void buildVisualizations(String serviceName, File contentFile) {
        FileInputStream inputStream = null;
		try {
            inputStream = new FileInputStream(contentFile);
            OMElement node = (OMElement)XMLUtils.toOM(inputStream, true);
			Collection<Visualization> visualizations = this.visualizationFactory.getVisualizations(node);
			for (Visualization viz : visualizations) {
				VisualizationManager.getInstance().addVisualization(serviceName, viz);							
			}						
		} catch (FileNotFoundException e) {
			throw new RuntimeException("File not found.", e);
		} catch (XMLStreamException e) {
			throw new RuntimeException(e);
		} finally {
            closeStream(inputStream);
        }
	}	

	protected ClassLoader getClassLoader() {
		return Thread.currentThread().getContextClassLoader();
	}

	private AxisService createAxisService(String name, Mashup mashup, URL url,
			ClassLoader classLoader) {
		AxisService axisService = null;

		try {
			log.debug("Creating axis service for " + name);

			MashupMessageReceiver messageReceiver = new MashupMessageReceiver(mashup);
			messageReceiver.setReader( createMessageContextReader(mashup) );
			
			this.setMediaTypes(messageReceiver, mashup);

			AxisServiceFactory factory = new AxisServiceFactory(
					NAMESPACE_PREFIX, NAMESPACE, configCtx, mashupFacade);

			axisService = factory.createAxisService(name, messageReceiver, url,
					classLoader, getMashupOperations(mashup));

			axisService.setFileName(url);
			axisService.setName(name);
			final String documentation = mashup.getDocumentation();
			if(documentation != null && !documentation.isEmpty()) {
				axisService.setDocumentation(documentation);
			}
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			throw new RuntimeException(e);
		}

		return axisService;
	}

	private MessageContextReader createMessageContextReader(Mashup mashup) {
		return new CompositeMessageContextReader(
                CookieMessageContextReader.getInstance(),
				RequestHeadersMessageContextReader.getInstance(),
				BodyMessageContextReader.getInstance(),
				createRestParameterMessageContextReader(mashup),
				HttpMethodMessageContextReader.getInstance(),
				URLMessageContextReader.getInstance(),
				RequestAttributesMessageContextReader.getInstance()
		);
	}

	private MessageContextReader createRestParameterMessageContextReader(Mashup mashup) {
		RestParametersMessageContextReader reader = new RestParametersMessageContextReader();

		try {
			XPathExpression expr = XPathUtil.createXPathExpression("/mx:mashup/mx:with[@restIndex!='']", XPathUtil.getContext());
			NodeList withs = (NodeList) expr.evaluate(mashup.getMashupXml(), XPathConstants.NODESET);

			for (int i = 0; i < withs.getLength(); i++) {
				Node with = withs.item(i);
				String name = with.getAttributes().getNamedItem("name").getTextContent();
				String idx = with.getAttributes().getNamedItem("restIndex").getTextContent();
				reader.setRestParameterName(Integer.parseInt(idx,10), name);
			}
		}
		catch (XPathExpressionException e) {
			log.error("error while processing rest parameters: " + e.getMessage(), e);
		}

		return reader;
	}
	
	private void setMediaTypes(MashupMessageReceiver msgReciever, Mashup mashup){
		try {
			XPathExpression expr = XPathUtil.createXPathExpression("/mx:mashup/mx:publish[@mediaType!='']", XPathUtil.getContext());
			NodeList pubs = (NodeList) expr.evaluate(mashup.getMashupXml(), XPathConstants.NODESET);

			for (int i = 0; i < pubs.getLength(); i++) {
				Node pub = pubs.item(i);
				String name = pub.getAttributes().getNamedItem("name").getTextContent();
				String value = pub.getAttributes().getNamedItem("mediaType").getTextContent();
				msgReciever.addMediaType(name, value);
			}
		}
		catch (XPathExpressionException e) {
			log.error("error while processing media types: " + e.getMessage(), e);
		}

	}

	protected List<String> getMashupOperations(Mashup mashup) {
		List<String> list = new ArrayList<String>();
		list.add("runMashup");
		if (mashup.isParallel()) {
			list.add("runConcurrent");
		}
		return list;
	}

	protected boolean isMashupFile(File file) {
		String ext = DeploymentFileData
				.getFileExtension(file.getAbsolutePath());
		return (ext.equalsIgnoreCase(MASHUP_FILE_EXTENSION));
	}

	protected String getMashupName(File file) {
		String shortName = Utils.getShortFileName(file.getAbsolutePath());
		int extLength = MASHUP_FILE_EXTENSION.length() + 1;
		return shortName.substring(0, shortName.length() - extLength);
	}

	public void init(ConfigurationContext configCtx) {
		log.debug("Initializing mashup deployer...");
		this.configCtx = configCtx;
	}

	public void setDirectory(String directory) {
		this.directory = directory;
	}

	public void setExtension(String extension) {
		MASHUP_FILE_EXTENSION = extension;
	}
	
	private void storeFaultyService(DeploymentFileData deploymentFileData,
			Throwable t) {
		StringWriter errorWriter = new StringWriter();
		PrintWriter ptintWriter = new PrintWriter(errorWriter);
		t.printStackTrace(ptintWriter);
		String error = "Error:\n" + errorWriter.toString();
		configCtx.getAxisConfiguration().getFaultyServices().put(
			deploymentFileData.getFile().getAbsolutePath(), error);
	}

	//Identical to unDeploy, except that the import map is not modified since the undeploy is temporary
	public void unDeployDependent(String fileName) throws DeploymentException {
        unDeploy(fileName, false);
	}

    public void unDeploy(String fileName) throws DeploymentException {
        unDeploy(fileName, true);
    }

	protected void unDeploy(String fileName, boolean updateImportMap) throws DeploymentException {
		File mashupFile = new File(fileName);
		log.debug("Undeploying mashup named: " + fileName);
		if (isMashupFile(mashupFile)) {
			String mashupName = getMashupName(mashupFile);
			try {
				MashupLocations.getInstance().remove(mashupName);
				configCtx.getAxisConfiguration().removeService(mashupName);
				log.debug(Messages.getMessage(DeploymentErrorMsgs.SERVICE_REMOVED, mashupName));

                if ( updateImportMap ) {
				    MashupDependentsMap.getInstance().removeDependent(mashupName);
                }
			} catch (AxisFault axisFault) {
				// May be a faulty service
				log.error(Messages.getMessage(
						DeploymentErrorMsgs.FAULTY_SERVICE_REMOVAL, axisFault
								.getMessage()), axisFault);
				//configCtx.getAxisConfiguration().removeFaultyService(mashupName);
				removeFaultyService(new File(fileName));
			}
		}
	}
	
	private void removeFaultyService (File file){
		final String fileName = Utils.getShortFileName(file.getAbsolutePath());
		configCtx.getAxisConfiguration().removeFaultyService(fileName);
	}

	protected boolean matchesDeploymentExclusionPattern(String mashupName, Properties props) {
		Pattern pattern = Pattern.compile(
			props.getProperty(PROP_DEPLOYMENT_EXCLUSION_PATTERN, DEFAULT_DEPLOYMENT_EXCLUSION_PATTERN),
			Pattern.CASE_INSENSITIVE
		);
		
		if ( pattern.matcher(mashupName).matches() ) {
			return true;
		}

		if ( "exclude".equalsIgnoreCase(props.getProperty(mashupName))) {
			return true;
		}

		return false;
	}

	protected Properties getDeployerProperties(String deploymentFolder) {
		Properties props = new Properties();
		FileReader reader = null;

		try {
			File propfile = new File(deploymentFolder+"/MashupDeployer.properties");

			if ( propfile.exists() && propfile.canRead() ) {
				reader = new FileReader(propfile);
				props.load(reader);
			}
		}
		catch (IOException e) {
			log.warn(e.getMessage(), e);
		}
		finally {
			if ( reader != null ) {
				try {
					reader.close();
				}
				catch (IOException e) {}
			}
		}

		return props;
	}

    private String resolveMashupDirectory() {
        String dir = this.directory;

        if (dir.startsWith("${")) {
            final String sysprop = dir.substring(2, dir.length() -1);
            dir = System.getProperty(sysprop);
        }

        return dir;
    }
}
